1 module hip.gui.widget;
2 
3 class Widget
4 {
5     struct Bounds
6     {
7         int x, y, width, height;
8     }
9     struct Transform
10     {
11         int x, y;
12         float rotation = 0, scaleX = 0, scaleY = 0;
13     }
14     int width, height;
15 
16     protected Widget parent;
17     protected Widget[] children;
18     protected Transform worldTransform;
19     protected Transform localTransform;
20     protected bool visible = true;
21     protected bool isDirty = true;
22 
23     Bounds getWorldBounds()
24     {
25         Bounds b = Bounds(worldTransform.x, worldTransform.y, width, height);
26         Bounds unmod = b;
27         import hip.api;
28         foreach(ch; children)
29         {
30             import hip.math.utils:min, max;
31             Bounds chBounds = ch.getWorldBounds;
32             b.x = min(b.x, chBounds.x);
33             b.y = min(b.y, chBounds.y);
34             b.width = max(b.width, chBounds.width+chBounds.x - unmod.x);
35             b.height = max(b.height, chBounds.height+chBounds.y - unmod.y);
36         }
37         return b;
38     }
39 
40     Widget findWidgetAt(float[2] pos){return findWidgetAt(cast(int)pos[0], cast(int)pos[1]);}
41     Widget findWidgetAt(int x, int y)
42     {
43         import hip.math.collision;
44         foreach_reverse(w; children)
45         {
46             Bounds wb = w.getWorldBounds();
47             if(w.visible && isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height))
48                 return w.findWidgetAt(x, y);
49         }
50 
51         Bounds wb = getWorldBounds();
52         return isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height) ? this : null;
53     }
54 
55     Bounds getLocalBounds(){return Bounds(localTransform.x,localTransform.y,width,height);}
56 
57     void setPosition(int x, int y)
58     {
59         isDirty = true;
60         localTransform.x = x;
61         localTransform.y = y;
62         setChildrenDirty();
63     }
64 
65     private void setChildrenDirty()
66     {
67         foreach(ch; children)
68         {
69             ch.isDirty = true;
70             ch.setChildrenDirty();
71         }
72     }
73 
74     private Widget getDirtyRoot()
75     {
76         Widget curr = parent;
77         Widget last = curr;
78         while(curr && curr.isDirty)
79         {
80             last = curr;
81             curr = curr.parent;
82         }
83         return curr is null ? last : curr;
84     }
85 
86     private void updateWorldTransform(in Transform* parentTransform)
87     {
88         if(parentTransform is null)
89             worldTransform = localTransform;
90         else
91         {
92             alias p = parentTransform;
93             worldTransform.x = p.x+localTransform.x;
94             worldTransform.y = p.y+localTransform.y;
95             worldTransform.rotation = p.rotation+localTransform.rotation;
96             worldTransform.scaleX = p.scaleX*localTransform.scaleX;
97             worldTransform.scaleY = p.scaleY*localTransform.scaleY;
98         }
99         isDirty = false;
100         foreach(ch; children)
101             ch.updateWorldTransform(&worldTransform);
102     }
103     private void recalculateWorld()
104     {
105         if(isDirty)
106         {
107             Widget root = getDirtyRoot();
108             if(root)
109                 root.updateWorldTransform(root.parent ? &root.parent.worldTransform : null);
110             else
111                 updateWorldTransform(parent ? &parent.worldTransform : null);
112         }
113     }
114 
115     void addChild(scope Widget[] widgets...)
116     {
117         foreach(w; widgets) addChild(w);
118     }
119     void addChild(Widget w)
120     {
121         children~= w;
122         w.isDirty = true;
123         w.parent = this;
124         w.setChildrenDirty();
125     }
126 
127     void setParent(Widget w)
128     {
129         w.addChild(this);
130     }
131 
132     //Event Methods
133         void onFocusEnter()
134         {
135             isFocused = true;
136         }
137         void onFocusExit()
138         {
139             isFocused = false;
140         }
141 
142         void onScroll(float[3] currentScroll, float[3] lastScroll)
143         {
144             setPosition(
145                 cast(int)(localTransform.x + currentScroll[0] - lastScroll[0]),
146                 cast(int)(localTransform.y + currentScroll[1] - lastScroll[1])
147             );
148         }
149         ///Executed the first time the mouse enters in the widget's boundaries
150         void onMouseEnter(){}
151         ///Executed when the mouse goes down inside the widget
152         void onMouseDown(){}
153         ///Executed when both a mousedown and mouseup is executed when mouse is over this widget
154         void onMouseClick(){}
155         ///If onMouseDown was executed, onMouseUp will be called even if the mouse is not inside the widget
156         void onMouseUp(){}
157         void onMouseMove(){}
158         private int dragOffsetX, dragOffsetY;
159         void onDragStart(int x, int y)
160         {
161             dragOffsetX = worldTransform.x - x;
162             dragOffsetY = worldTransform.y - y;
163         }
164         void onDragged(int x, int y)
165         {
166             import hip.api;
167             setPosition(x + dragOffsetX, y + dragOffsetY);
168         }
169         void onDragEnd(){}
170         ///Returns whether it accepted the receive
171         bool onDropReceived(Widget w){return false;}
172         void onMouseExit(){}
173         bool isDraggable;
174         bool isFocused;
175     //End Event Methods
176 
177 
178     void update()
179     {
180         foreach(ch; children) ch.update();
181     }
182 
183     protected void preRender(){recalculateWorld();}
184     final void render()
185     {
186         preRender();
187         onRender();
188     }
189     void onRender(){foreach(ch; children) if(ch.visible) ch.render();}
190 }
191 
192 interface IWidgetRenderer
193 {
194     void render(int x, int y, int width, int height);
195 }
196 
197 class DebugWidgetRenderer : IWidgetRenderer
198 {
199     import hip.api.graphics.color;
200     import hip.math.random;
201     HipColor color;
202     this()
203     {
204         color[] = Random.rangeub(0, 255);
205     }
206     this(HipColor color){this.color = color;}
207 
208     void render(int x, int y, int width, int height)
209     {
210         import hip.api.graphics.g2d.renderer2d;
211         fillRoundRect(x,y,width,height, 4, color);
212     }
213 }